/**
 * @fileOverview Game sandbox
 *
 * @author Mikhail Davydov
 *
 */

/**#nocode+*/
(function (window, $) {
/**#nocode-*/

    /**
     * Utils
     *
     * @exports Utils as Libs.Utils
     * @namespace Utils
     */
    var Utils = {
        /**
         * Augment function
         *
         * @param {Object} receivingClass
         * @param {Object} givingClass
         *
         * @returns {Object} receivingClass
         */
        augment: function (receivingClass, givingClass /*, method1, method2*/) {
            var methodName,
                i,
                len;
            if(arguments[2]) { // Only give certain methods.
                for(i = 2, len = arguments.length; i < len; i++) {
                    receivingClass[arguments[i]] = givingClass[arguments[i]];
                }
            } else { // Give all methods.
                for(methodName in givingClass) {
                    receivingClass[methodName] = givingClass[methodName];
                }
            }
            return receivingClass;
        },
        /**
         * Inherit function
         *
         * @param {Object} child
         * @param {Object} parent
         *
         * @returns {Object}
         */
        inherit: function (child, parent) {
            function F() {}
            F.prototype = parent.prototype;
            child.prototype = new F();
            child.prototype.constructor = child;
            child.__proto__ = parent.prototype;
            return child;
        }
    };

    /**
     * Game
     *
     * @namespace Game
     */
    var Game = {
        MAX_TURNS: 500,
        MAX_WORKER_TIMEOUT: 2000,
        MAX_HP_AMOUNT: 10,
        MAX_SHIELD_AMOUNT: 2,
        SHIELD_REGEN_AMOUNT: 1,
        MAX_AP: 4,
        /**
         * Returns empty field map
         * <pre>
         * 1 - floor
         * 0 - wall
         * </pre>
         * @returns {Array[]}
         */
        map: function () {
            return [
            // X 0 1 2 3 4 5 6 7 8 9  // Y
                [0,0,0,0,0,0,0,0,0,0],// 0
                [0,1,1,1,1,1,1,1,1,0],// 1
                [0,1,1,1,1,1,1,1,1,0],// 2
                [0,1,1,1,1,1,1,1,1,0],// 3
                [0,1,1,1,1,1,1,1,1,0],// 4
                [0,1,1,1,1,1,1,1,1,0],// 5
                [0,1,1,1,1,1,1,1,1,0],// 6
                [0,1,1,1,1,1,1,1,1,0],// 7
                [0,1,1,1,1,1,1,1,1,0],// 8
                [0,0,0,0,0,0,0,0,0,0] // 9
            ];
        },
        /**
         * List of players
         *
         * @type Object[]
         */
        players: [],
        /**
         * List of worker files
         *
         * @type String[]
         */
        workersFiles: [],
        /**
         * Relplay list
         *
         * @type Object[]
         */
        replay: [],
        /**
         * Turn of player id
         *
         * @type Number
         */
        turnOf: null,
        /**
         * Log level
         *
         * @type Number
         */
        logLevel: 0,
        /**
         * Inits player position
         *
         * @returns {Object}
         */
        initPosition: function () {
            // direction
            //     0
            //   3 # 1
            //     2
            //
            // field of view
            //       X
            //      XXX
            //     XX^XX  direction up (0)
            //      X#X
            //       X
            //
            //       X
            //      X#X
            //     XXvXX  direction down (2)
            //      XXX
            //       X
            //
            //       X
            //      XXX
            //     XX<#X  direction left (3)
            //      XXX
            //       X
            //
            //       X
            //      XXX
            //     X#>XX  direction right (1)
            //      XXX
            //       X

            return {
                x: ~~(Math.random() * 8) + 1,
                y: ~~(Math.random() * 8) + 1,
                direction: 0
            };
        },
        /**
         * Registers player
         *
         * @param {String} webWorkerFile
         *
         * @returns {String|Number} player name
         */
        registerPlayer: function (webWorkerFile) {
            var position = this.initPosition(),
                worker = new Worker(webWorkerFile),
                player = {worker: worker, health: Game.MAX_HP_AMOUNT, shield: Game.MAX_SHIELD_AMOUNT, name: this.players.length},
                self = this;

            Utils.augment(player, position);

            worker.onerror = function (e) {
                self.log(e);
            };
            this.players.push(player);
            this.workersFiles.push(webWorkerFile);
            return player.name;
        },
        /**
         * Returns map alive with players
         *
         * @returns {Array[]}
         */
        getCurrentMap: function () {
            var map = this.map(), i, c, t;
            for (i = 0, c = this.players.length; i < c; i += 1) {
                t = this.players[i];
                if (this.players[i].health > 0) {
                    map[t.y][t.x] = {
                        name: i
                    };
                }
            }
            return map;
        },
        /**
         * Gets all objects in dof of bot
         *
         * @returns {Object[]}
         */
        getObjectsInFieldOfView: function (x, y, direction) {
            var map = this.getCurrentMap(), fowShiftX, fowShiftY, fow, i, c, t,
            objects = [];
            switch (direction) {
                case 0:
                    fowShiftX = 0;
                    fowShiftY = -1;
                    break;
                case 1:
                    fowShiftX = 1;
                    fowShiftY = 0;
                    break;
                case 2:
                    fowShiftX = 0;
                    fowShiftY = 1;
                    break;
                case 3:
                    fowShiftX = -1;
                    fowShiftY = 0;
                    break;
            }
            // getting fow field
            fow = [
                {x: 0 + x + fowShiftX,  y: 0 + y + fowShiftY},
                {x: 1 + x + fowShiftX,  y: 0 + y + fowShiftY},
                {x: 0 + x + fowShiftX,  y: 1 + y + fowShiftY},
                {x: 1 + x + fowShiftX,  y: 1 + y + fowShiftY},
                {x: 0 + x + fowShiftX,  y: -1 + y + fowShiftY},
                {x: -1 + x + fowShiftX, y: 0 + y + fowShiftY},
                {x: -1 + x + fowShiftX, y: -1 + y + fowShiftY},
                {x: -1 + x + fowShiftX, y: 1 + y + fowShiftY},
                {x: 1 + x + fowShiftX,  y: -1 + y + fowShiftY},
                {x: 2 + x + fowShiftX,  y: 0 + y + fowShiftY},
                {x: -2 + x + fowShiftX, y: 0 + y + fowShiftY},
                {x: 0 + x + fowShiftX,  y: 2 + y + fowShiftY},
                {x: 0 + x + fowShiftX,  y: -2 + y + fowShiftY}
            ];

            for (i = 0, c = fow.length; i < c; i += 1) {
                t = fow[i];
                if (map[t.y] && typeof map[t.y][t.x] !== 'undefined' && map[t.y][t.x] !== 1) {
                    objects.push({
                        object: map[t.y][t.x],
                        x: t.x,
                        y: t.y
                    });
                }
            }
            return objects;
        },
        /**
         * Requests comand from Worker
         *
         * @param {Object}   player
         * @param {Number}   apLeft
         * @param {Function} playerActionCallback
         */
        getPlayerAction: function (player, apLeft, playerActionCallback) {
            var data = {}, fullData, message, self = this;

            data.fow = this.getObjectsInFieldOfView(player.x, player.y, player.direction);
            data.apLeft = apLeft;
            data.player = {
                x: player.x,
                y: player.y,
                name: player.name,
                direction: player.direction,
                health: player.health,
                shield: player.shield
            };

            fullData = {'call': 'callback', 'arguments': data};
            message = JSON.stringify(fullData);
            self.log(data, '    Callback for player ' + player.name);

            if (player.health > 0) {
                player.worker.onmessage = function (e) {
                    var actionData = JSON.parse(e.data),
                        groupName = '    [' + (self.replay.length + 1) + '] Action ' + actionData.action + ' of player ' + player.name;
                    self.log(actionData, groupName);
                    playerActionCallback(actionData, data);

                    self.replay.push({
                        player: data,
                        action: actionData
                    });
                };
                player.worker.postMessage(message);
            } else { // dead action
                player.worker.onmessage = function () {};
                var actionData = {action: 'dead'},
                    groupName = '    [' + (self.replay.length + 1) + '] Action ' + actionData.action + ' of player ' + player.name;
                self.log(actionData, groupName);
                playerActionCallback(actionData, data);

                self.replay.push({
                    player: data,
                    action: actionData
                });
            }
        },
        /**
         * Performs players action
         *
         * @param {Object} player
         * @param {Object} action
         */
        performAction: function (player, action) {
            // action.action
            // action.options
            // action.direction
            if (typeof action !== 'object' || typeof action.action !== 'string') {
                return;
            }

            if (typeof Actions[action.action] === 'object' && Actions[action.action].callable) {
                Actions[action.action].callback.call(this, player, action.options);
            }

            if (typeof action.direction === 'number') {
                action.direction = ~~action.direction;
                if (action.direction >=0 && action.direction <= 3) {
                    player.direction = action.direction;
                }
            }
        },
        /**
         * Gets AP cost of action
         *
         * @param {Object} action
         *
         * @returns {Number}
         */
        getActionCost: function (action) {
            if (typeof action !== 'object') {
                return 1;
            }
            if (typeof Actions[action.action] === 'object' && Actions[action.action].callable) {
                return Actions[action.action].cost;
            } else {
                return 1;
            }
        },
        /**
         * Player turn
         *
         * @param {Object}   currentPlayer
         * @param {Number}   actionPointsLeft
         * @param {Function} turnEndCallback
         */
        playerTurn: function (currentPlayer, actionPointsLeft, turnEndCallback) {
            var actionCost,
                self = this,
                timerId;

            timerId = window.setTimeout(function () {
                // remove worker listener
                currentPlayer.worker.onmessage = function () {};
                actionPointsLeft = 0;
                turnEndCallback();
                self.log('Timeout of player ' + currentPlayer.name);
            }, Game.MAX_WORKER_TIMEOUT);
            
            this.getPlayerAction(currentPlayer, actionPointsLeft, function (action, data) {
                window.clearTimeout(timerId);
                actionCost = self.getActionCost(action);
                actionPointsLeft -= actionCost;
                if (actionPointsLeft >= 0) {
                    self.performAction(currentPlayer, action);
                } else {
                    self.emitEvent(currentPlayer.name, 'onFailAction', {action: action, actionCost: actionCost});
                }
                self.onEndOfPlayerAction(currentPlayer);
                if (!self.isEndOfGame() && actionPointsLeft > 0) {
                    self.playerTurn(currentPlayer, actionPointsLeft, turnEndCallback);
                } else {
                    turnEndCallback();
                }
            });
        },
        /**
         * Detects end of game
         *
         * @returns {Boolean}
         */
        isEndOfGame: function () {
            for (var i = 0, c = this.players.length, cnt = 0; i < c; i += 1) {
                if (this.players[i].health <= 0) {
                    cnt += 1;
                }
            }
            return cnt === (c - 1);
        },
        /**
         * Regens shields
         */
        regenShields: function () {
            for (var i = 0, c = this.players.length; i < c; i += 1) {
                this.players[i].shield += Game.SHIELD_REGEN_AMOUNT;
                if (this.players[i].shield > Game.MAX_SHIELD_AMOUNT) {
                    this.players[i].shield = Game.MAX_SHIELD_AMOUNT;
                } else { // only if shield is regened emit event
                    this.emitEvent(i, 'onShieldRegen', Game.SHIELD_REGEN_AMOUNT);
                }
            }
        },
        /**
         * Common callback
         */
        playerTurnCallback: function () {
            var player = this.players[this.turnOf], self = this, found = false, trysCount = 0;
            this.onEndOfPlayerTurn(player);
            this.maxTurns -= 1;
            if (this.isEndOfGame() || !this.maxTurns) {
                this.emitEvent(null, 'onEnd');
                this.onEndOfGame();
            } else {
                // next player detector
                while (!found) {
                    this.turnOf = this._getNextPlayerName(this.turnOf);
                    if (this.players[this.turnOf].health > 0) {
                        found = true;
                    }
                    trysCount += 1;
                    if (trysCount > this.players.length) {
                        throw new Error('Loop');
                    }
                }

                player = this.players[this.turnOf];
                this.playerTurn(player, Game.MAX_AP, function () {
                    self.playerTurnCallback.call(self);
                });
            }
        },
        _getNextPlayerName: function (currentPlayer) {
            currentPlayer += 1;
            if (currentPlayer >= this.players.length) {
                currentPlayer = 0;
                this.currentPhase += 1;
                this.onEndOfPhase(this.currentPhase);
            }
            return currentPlayer;
        },
        /**
         * Starts the game
         *
         * @param {Object} options
         * @param {Number} [options.logLevel = 0] log level
         *                 0 - no log
         *                 1 - messages only
         *                 2 - data and messages
         */
        start: function (options) {
            if (!options) {
                options = {};
            }
            this.logLevel = (options.logLevel || this.logLevel) * 1;
            var self = this, player;
            this.maxTurns = Game.MAX_TURNS;
            this.currentPhase = 0;
            this.turnOf = 0;
            this.emitEvent(null, 'onStart');
            player = this.players[this.turnOf];
            this.playerTurn(player, Game.MAX_AP, function () {
                self.playerTurnCallback.call(self);
            });
        },
        /**
         * Emits event
         * 
         * @param {Number|Null} targetId player id
         * @param {String}      eventId
         * @param {Mixed}       param
         */
        emitEvent: function (targetId, eventId, param) {
            var data = {'call': eventId, 'arguments': param},
                message = JSON.stringify(data),
                groupName;
            if (targetId !== null) {
                if (this.players[targetId] && this.players[targetId].worker) {
                    this.players[targetId].worker.postMessage(message);
                }
            } else {
                for (var i = 0, c = this.players.length; i < c; i += 1) {
                    if (this.players[i] && this.players[i].worker) {
                        this.players[i].worker.postMessage(message);
                    }
                }
            }
            
            groupName = '>>> Event ' + eventId + ' on ' + (targetId === null ? 'all' : 'player ' + targetId);
            this.log(data, groupName);
        },
        /**
         * @event
         */
        onEndOfPlayerTurn: function (player) {
            
        },
        /**
         * @event
         */
        onEndOfPlayerAction: function (player) {

        },
        /**
         * @event
         */
        onEndOfPhase: function () {
            this.regenShields();
        },
        /**
         * @event
         */
        onEndOfGame: function () {
            this.log(this.replay, 'Replay');
            ReplayRender.init('field', Game);
            ReplayRender.frame(0);
        },
        /**
         * Log function
         *
         * @param {Mixed}  data
         * @param {String} groupName
         */
        log: function (data, groupName) {
            if (this.logLevel) {
                if (this.logLevel === 1) {
                    console.log(groupName);
                }
                if (this.logLevel === 2) {
                    if (groupName) {
                        console.groupCollapsed(groupName);
                        console.dir(data);
                        console.groupEnd(groupName);
                    } else {
                        console.log(data);
                    }
                }
            }
        }
    };

    /**
     * Possuble bots actions
     *
     * @namespace Actions
     */
    var Actions = {
        shoot: {
            /**
             * Shoot action
             *
             * this parameter must be Game object
             *
             * @param {Object} player
             * @param {Object} options
             * @param {Number} options.x
             * @param {Number} options.y
             */
            callback: function (player, options) {
                // proxy action
                Actions.commonHitAction.call(this, player, options, 2, 5);
            },
            cost: 2,
            callable: true
        },
        stab: {
            /**
             * Stab action
             *
             * this parameter must be Game object
             *
             * @param {Object} player
             * @param {Object} options
             * @param {Number} options.x
             * @param {Number} options.y
             */
            callback: function (player, options) {
                // proxy action
                Actions.commonHitAction.call(this, player, options, 3, 1);
            },
            cost: 2,
            callable: true
        },
        move: {
            /**
             * Move action
             *
             * this parameter must be Game object
             *
             * @param {Object} player
             * @param {Object} options
             * @param {Number} options.x
             * @param {Number} options.y
             */
            callback: function (player, options) {
                var x, y, map = this.getCurrentMap();
                if (typeof options !== 'object' ||
                    typeof options.x !== 'number' ||
                    typeof options.y !== 'number') {
                    return;
                }

                // only int possible
                options.x = ~~options.x;
                options.y = ~~options.y;

                // only 1 point posible
                if (Math.abs(player.x - options.x) + Math.abs(player.y - options.y) === 1) {
                    x = options.x;
                    y = options.y;
                }

                // check point and move
                if (map[y] && typeof map[y][x] !== 'undefined' && map[y][x] === 1) {
                    player.x = x;
                    player.y = y;
                    this.emitEvent(player.name, 'onAfterMove', {x: x, y: y});
                }
            },
            cost: 1,
            callable: true
        },
        dead: {
            callback: function () {},
            cost: 4,
            callable: false
        },
        /**
         * Called from hit actions
         */
        commonHitAction: function (player, options, damage, range) {
            var x, y, map = this.getCurrentMap(), dx, dy, target, name;
            if (typeof options !== 'object' ||
                typeof options.x !== 'number' ||
                typeof options.y !== 'number') {
                return;
            }

            // only int possible
            options.x = ~~options.x;
            options.y = ~~options.y;

            // only "range" points range posible
            dx = player.x - options.x;
            dy = player.y - options.y;
            if (Math.sqrt(dx * dx + dy * dy) > range) {
                return;
            }
            x = options.x;
            y = options.y;
            // check point and move
            if (map[y] && typeof map[y][x] === 'object') {
                name = map[y][x].name;
                target = this.players[name];
                target.shield -= damage;
                if (target.shield < 0) {
                    target.health += target.shield;
                }
                if (target.shield < 0) {
                    target.shield = 0;
                }
                this.emitEvent(player.name, 'onHit', {damage: damage, x: target.x, y: target.y, name: target.name});
                this.emitEvent(target.name, 'onDamage', {damage: damage, health: target.health, shield: target.shield, x: player.x, y: player.y, name: player.name});
                if (target.health <=0) {
                    this.emitEvent(player.name, 'onKill', {name: target.name});
                    this.emitEvent(target.name, 'onDead');
                }
            }
        }
    };

    var ReplayRender = {
        replay: [],
        game: {},
        currnetFrame: 0,
        timerId: null,
        init: function (target, game) {
            var i, c, view = '', j, k, bot;
            this.replay = game.replay;
            this.game = game;
            if (typeof target === 'string') {
                target = window.document.getElementById(target);
            }

            this.mapH = game.map().length;
            this.mapW = game.map()[0].length;

            view += '<div><ol>';
            for (i = 0, c = game.workersFiles.length; i < c; i++) {
                bot = game.workersFiles[i].split('/').pop().split('.')[0];
                view += '<li><a href="http://pastebin.com/' + bot + '" target="_blank">Source code of bot ' + bot + '</a></li>';
            }
            view += '</ol></div>';
            view += '<div style="clear:both"><button onclick="ReplayRender.timer(); return false;">Timer On/Off</button> <span id="total"></span></div>';
            view += '<div id="main-field" style="position: absolute;width:' + (this.mapW * 51) + 'px;height:' + (this.mapH * 51) + 'px;margin:5px;">';
            for (j = 0; j < this.mapH; j++) {
                for (k = 0; k < this.mapW; k++) {
                    view += '<div id="cell_' + j + '_' + k + '" style="left:' + (k * 51) + 'px;top:' + (j * 51) + 'px;position:absolute;width:50px;height:50px;border-bottom: 1px solid #fff; border-right: 1px solid #fff;" class="fog">&nbsp;</div>';
                }
            }
            view += '</div>';
            view += '<div id="overlay-field" style="width:' + (this.mapW * 51) + 'px;height:' + (this.mapH * 51) + 'px;margin: 5px; position: absolute;">';
            view += '</div>';
            target.innerHTML = view;
        },
        frame: function (id) {
            var i, c, j, k, frame, t, name, className, element, clone;

            frame = this.replay[id];
            if (!frame) {
                return;
            }

            name = frame.player.player.name;

            for (j = 0; j < this.mapH; j++) {
                for (k = 0; k < this.mapW; k++) {
                    element = window.document.getElementById('cell_' + j + '_' + k);
                    element.innerHTML = '&nbsp;';
                    element.title = '';
                    element.className = 'fog';
                }
            }
            // action
            if (frame.player.fow) {
                for (i = 0, c = frame.player.fow.length; i < c; i++) {
                    t = frame.player.fow[i];
                    className = (typeof t.object === 'object') ? 'man' : 'wall';
                    if (typeof t.object.name !== 'undefined') {
                        className += ' man-' + t.object.name;
                    }
                    element = window.document.getElementById('cell_' + t.y + '_' + t.x);
                    element.className = className;
                }
            }

            element = window.document.getElementById('cell_' + frame.player.player.y + '_' + frame.player.player.x);
            element.innerHTML = '<div class="' + 'man-' + name + ' man direction-' + frame.player.player.direction + '"><span class="' + frame.action.action + '">hp:' + frame.player.player.health + '(' + frame.player.player.shield + ')</span></div>';
            element.className = 'fog';

            // animation
            switch (frame.action.action) {
                case 'move':
                    $(element).find('div').animate({
                        top: ((frame.action.options.y - frame.player.player.y) * 51) + 'px',
                        left: ((frame.action.options.x - frame.player.player.x) * 51) + 'px'
                    }, 500);
                    break;
                case 'shoot':
                case 'stab':
                    $('#overlay-field').html('<div class="ball" style="' + 'left:' + (frame.player.player.x * 51) + 'px;' + 'top:' + (frame.player.player.y * 51) + 'px;' + 'position: absolute; z-index: 2;"></div>');
                    $('#overlay-field').find('div').animate({
                        top: (frame.action.options.y * 51) + 'px',
                        left: (frame.action.options.x * 51) + 'px'
                    }, 500, function () {
                        $('#overlay-field').html('');
                    });
                    break;
            }
        },
        nextFrame: function () {
            this.currnetFrame += 1;
            if (this.currnetFrame >= this.replay.length) {
                return;
            }
            this.frame(this.currnetFrame);
            window.document.getElementById('total').innerHTML = (this.currnetFrame + 1) + '/' + (this.replay.length);
        },
        timer: function () {
            var self = this;
            if (this.timerId) {
                window.clearInterval(this.timerId);
                this.timerId = null;
            } else {
                this.timerId = window.setInterval(function() {
                    self.nextFrame();
                }, 600);
            }
        }
    };

    var Sandbox = {
        registerPlayer: function (playerData) {
            return Game.registerPlayer(playerData);
        },
        start: function (options) {
            // harakiri
            Sandbox.start = function () {};
            Game.start(options);
        }
    };

    window.ReplayRender = ReplayRender;
    window.Sandbox = Sandbox;

/*global window */
/**#nocode+*/
}(window, window.jQuery));
/**#nocode-*/